
using System;
using System.Security.Cryptography;

namespace ZXW.DotNet.Common.NetSnmp
{
	/// <summary>DES privacy protocol implementation class.</summary>
	/// <remarks>
	/// SNMP Version 3 DES privacy protocol implementation.
	/// 
	/// DES requires an encryption key be provided of 16 bytes in length. Class will accept longer key values (which will be
	/// trimmed to 16 bytes) but both encrypt and decrypt operations will fail if key is shorter then required value length.
	/// 
	/// Decryption operation depends on USM header privacyParameters field value which is generated by the Encrypt method. Make
	/// sure privacyParameters argument value is correctly inserted into the target packet to enable SNMP agent to decrypt the
	/// message.
	/// </remarks>
	public class PrivacyDES: IPrivacyProtocol
	{
		/// <summary>
		/// Internal salt value. As per RFC standard, salt value is initialized in the constructor and incremented by 1 for each
		/// subsequent packet.
		/// </summary>
		protected Int32 _salt = 0;
		/// <summary>
		/// Standard constructor.
		/// </summary>
		public PrivacyDES()
		{
			Random random = new Random();
			_salt = random.Next(1, Int32.MaxValue);
		}
		/// <summary>
		/// Returns next salt value.
		/// </summary>
		/// <returns>32-bit integer salt value in network byte order (big endian)</returns>
		protected int NextSalt()
		{
			if (_salt == Int32.MaxValue)
				_salt = 1;
			else
				_salt += 1;
			return _salt;
		}
		/// <summary>
		/// Encrypt ScopedPdu using DES encryption protocol
		/// </summary>
		/// <param name="unencryptedData">Unencrypted ScopedPdu byte array</param>
		/// <param name="offset">Offset to start encryption</param>
		/// <param name="length">Length of data to encrypt</param>
		/// <param name="key">Encryption key. Key has to be at least 32 bytes is length</param>
		/// <param name="engineBoots">Authoritative engine boots value</param>
		/// <param name="engineTime">Authoritative engine time value. Not used for DES</param>
		/// <param name="privacyParameters">Privacy parameters out buffer. This field will be filled in with information
		/// required to decrypt the information. Output length of this field is 8 bytes and space has to be reserved
		/// in the USM header to store this information</param>
		/// <param name="authDigest">Authentication digest class reference. Not used by DES and can be null.</param>
		/// <returns>Encrypted byte array</returns>
		/// <exception cref="ArgumentOutOfRangeException">Thrown when encryption key is null or length of the encryption key is too short.</exception>
		public byte[] Encrypt(byte[] unencryptedData, int offset, int length, byte[] key, int engineBoots, int engineTime, out byte[] privacyParameters, IAuthenticationDigest authDigest)
		{
			if (key == null || key.Length < MinimumKeyLength)
				throw new ArgumentOutOfRangeException("encryptionKey", "Encryption key length has to 32 bytes or more.");

			privacyParameters = GetSalt(engineBoots);
			byte[] iv = GetIV(key, privacyParameters);

			// DES uses 8 byte keys but we need 16 to encrypt ScopedPdu. Get first 8 bytes and use them as encryption key
			byte[] outKey = GetKey(key);


			int div = (int)Math.Floor(length / 8.0);
			if ((length % 8) != 0)
				div += 1;
			int newLength = div * 8;
			byte[] result = new byte[newLength];
			byte[] buffer = new byte[newLength];

			byte[] inbuffer = new byte[8];
			byte[] cipherText = iv;
			int posIn = 0;
			int posResult = 0;
			Buffer.BlockCopy(unencryptedData, offset, buffer, 0, length);

			DES des = new DESCryptoServiceProvider();
			des.Mode = CipherMode.ECB;
			des.Padding = PaddingMode.None;

			ICryptoTransform transform = des.CreateEncryptor(outKey, null);
			for (int b = 0; b < div; b++)
			{
				for (int i = 0; i < 8; i++)
				{
					inbuffer[i] = (byte)(buffer[posIn] ^ cipherText[i]);
					posIn++;
				}
				transform.TransformBlock(inbuffer, 0, inbuffer.Length, cipherText, 0);
				Buffer.BlockCopy(cipherText, 0, result, posResult, cipherText.Length);
				posResult += cipherText.Length;
			}

			des.Clear();

			return result;
		}

		/// <summary>
		/// Decrypt DES encrypted ScopedPdu
		/// </summary>
		/// <param name="encryptedData">Source data buffer</param>
		/// <param name="offset">Offset within the buffer to start decryption process</param>
		/// <param name="length">Length of data to decrypt</param>
		/// <param name="key">Decryption key. Key length has to be 32 bytes in length or longer (bytes beyond 32 bytes are ignored).</param>
		/// <param name="engineBoots">Authoritative engine boots value</param>
		/// <param name="engineTime">Authoritative engine time value</param>
		/// <param name="privacyParameters">Privacy parameters extracted from USM header</param>
		/// <returns>Decrypted byte array</returns>
		/// <exception cref="ArgumentNullException">Thrown when encrypted data is null or length == 0</exception>
		/// <exception cref="ArgumentOutOfRangeException">Thrown when encryption key length is less then 32 byte or if privacy parameters
		/// argument is null or length other then 8 bytes</exception>
		public byte[] Decrypt(byte[] encryptedData, int offset, int length, byte[] key, int engineBoots, int engineTime, byte[] privacyParameters)
		{
			if ((length % 8) != 0)
				throw new ArgumentOutOfRangeException("encryptedData", "Encrypted data buffer has to be divisible by 8.");
			if (encryptedData == null || encryptedData.Length == 0)
				throw new ArgumentNullException("cryptedData");
			if (privacyParameters == null || privacyParameters.Length != PrivacyParametersLength)
				throw new ArgumentOutOfRangeException("privacyParameters", "Privacy parameters argument has to be 8 bytes long");

			if (key == null || key.Length < MinimumKeyLength)
				throw new ArgumentOutOfRangeException("decryptionKey", "Decryption key has to be at least 16 bytes long.");

			byte[] iv = new byte[8];
			for (int i = 0; i < 8; ++i)
			{
				iv[i] = (byte)(key[8 + i] ^ privacyParameters[i]);
			}
			DES des = new DESCryptoServiceProvider();
			des.Mode = CipherMode.CBC;
			des.Padding = PaddingMode.Zeros;

			// .NET implementation only takes an 8 byte key
			byte[] outKey = new byte[8];
			Buffer.BlockCopy(key, 0, outKey, 0, 8);

			des.Key = outKey;
			des.IV = iv;
			ICryptoTransform transform = des.CreateDecryptor();

			byte[] decryptedData = transform.TransformFinalBlock(encryptedData, 0, encryptedData.Length);
			des.Clear();

			return decryptedData;
		}

		/// <summary>
		/// Returns minimum encryption/decryption key length. For DES, returned value is 16.
		/// </summary>
		/// <remarks>
		/// DES protocol requires an 8 byte encryption key and additional 8 bytes are used for generating the
		/// encryption IV.
		/// </remarks>
		public int MinimumKeyLength
		{
			get
			{
				return 16;
			}
		}
		/// <summary>
		/// Return maximum encryption/decryption key length. For DES, returned value is 16
		/// </summary>
		/// <remarks>
		/// DES protocol requires an 8 byte encryption key and additional 8 bytes are used for generating the
		/// encryption IV.
		/// </remarks>
		public int MaximumKeyLength
		{
			get { return 16; }
		}

		/// <summary>
		/// Get final encrypted length
		/// </summary>
		/// <param name="scopedPduLength">BER encoded ScopedPdu data length</param>
		/// <returns>Length of encrypted byte array</returns>
		public int GetEncryptedLength(int scopedPduLength)
		{
			if (scopedPduLength % 8 == 0)
			{
				return scopedPduLength;
			}
			return 8 * ((scopedPduLength / 8) + 1);
		}

		/// <summary>
		/// Returns the length of privacyParameters USM header field. For DES, field length is 8.
		/// </summary>
		public int PrivacyParametersLength
		{
			get
			{
				return 8;
			}
		}

		/// <summary>
		/// Operation not used by DES. Key length has to be 16 bytes of encryption/decryption operation will fail.
		/// 
		/// When called, shortKey is returned.
		/// </summary>
		/// <param name="shortKey">Encryption key</param>
		/// <param name="password">Privacy password</param>
		/// <param name="engineID">Authoritative engine id</param>
		/// <param name="authProtocol">Authentication protocol class instance</param>
		/// <returns>unaltered shortKey value</returns>
		public byte[] ExtendShortKey(byte[] shortKey, byte[] password, byte[] engineID, IAuthenticationDigest authProtocol)
		{
			return shortKey;
		}

		/// <summary>
		/// Privacy protocol name
		/// </summary>
		public string Name
		{
			get { return "DES"; }
		}

		/// <summary>
		/// DES implementation does NOT support extending of a short encryption key. Always returns false.
		/// </summary>
		public bool CanExtendShortKey
		{
			get
			{
				return false;
			}
		}

		/// <summary>
		/// Get DES encryption salt value. Salt value is generated by concatenating engineBoots value with
		/// the random integer value.
		/// </summary>
		/// <param name="engineBoots">SNMP engine boots value</param>
		/// <returns>Salt byte array 8 byte in length</returns>
		private byte[] GetSalt(int engineBoots)
		{
			byte[] salt = new byte[8]; // salt is 8 bytes
			int s = NextSalt();
			byte[] eb = BitConverter.GetBytes(engineBoots);
			// C# is little endian so reverse the values
			salt[3] = eb[0];
			salt[2] = eb[1];
			salt[1] = eb[2];
			salt[0] = eb[3];
			byte[] sl = BitConverter.GetBytes(s);
			salt[7] = sl[0];
			salt[6] = sl[1];
			salt[5] = sl[2];
			salt[4] = sl[3];
			return salt;
		}
		/// <summary>
		/// Extract and return DES encryption key.
		/// 
		/// Privacy password is 16 bytes in length. Only the first 8 bytes are used as DES password. Remaining
		/// 8 bytes are used as pre-IV value.
		/// </summary>
		/// <param name="privacyPassword">16 byte privacy password</param>
		/// <returns>8 byte DES encryption password</returns>
		/// <exception cref="SnmpPrivacyException">Thrown when privacy password is less then 16 bytes long</exception>
		private byte[] GetKey(byte[] privacyPassword)
		{
			if (privacyPassword == null || privacyPassword.Length < 16)
				throw new SnmpPrivacyException("Invalid privacy key length.");
			byte[] key = new byte[8];
			Buffer.BlockCopy(privacyPassword, 0, key, 0, 8);
			return key;
		}
		/// <summary>
		/// Generate IV from the privacy key and salt value returned by GetSalt method.
		/// </summary>
		/// <param name="privacyKey">16 byte privacy key</param>
		/// <param name="salt">Salt value returned by GetSalt method</param>
		/// <returns>IV value used in the encryption process</returns>
		/// <exception cref="SnmpPrivacyException">Thrown when privacy key is less then 16 bytes long.</exception>
		private byte[] GetIV(byte[] privacyKey, byte[] salt)
		{
			if (privacyKey.Length < 16)
				throw new SnmpPrivacyException("Invalid privacy key length");
			byte[] iv = new byte[8];
			for (int i = 0; i < iv.Length; i++)
			{
				iv[i] = (byte)(salt[i] ^ privacyKey[8+i]);
			}
			return iv;
		}
		/// <summary>
		/// Convert privacy password into encryption key using packet authentication hash.
		/// </summary>
		/// <param name="secret">Privacy user secret</param>
		/// <param name="engineId">Authoritative engine id of the snmp agent</param>
		/// <param name="authProtocol">Authentication protocol</param>
		/// <returns>Encryption key</returns>
		/// <exception cref="SnmpPrivacyException">Thrown when key size is shorter then MinimumKeyLength</exception>
		public byte[] PasswordToKey(byte[] secret, byte[] engineId, IAuthenticationDigest authProtocol)
		{
			// RFC 3414 - password length is minimum of 8 bytes long
			if (secret == null || secret.Length < 8)
				throw new SnmpPrivacyException("Invalid privacy secret length.");
			byte[] encryptionKey = authProtocol.PasswordToKey(secret, engineId);
			return encryptionKey;
		}
	}
}
